Skip to content

Add browser-compatible Worker polyfill and WPT coverage - #8

Draft
matthargett wants to merge 78 commits into
napi-v7from
feature/web-worker
Draft

Add browser-compatible Worker polyfill and WPT coverage#8
matthargett wants to merge 78 commits into
napi-v7from
feature/web-worker

Conversation

@matthargett

@matthargett matthargett commented Jul 21, 2026

Copy link
Copy Markdown
Member

Dependency and stack

This is the Worker follow-up to BabylonJS/JsRuntimeHost#189. It is built on the refreshed napi-v7 head (231fa40), which incorporates the issue-7 Node-API work, the latest audited CTS updates, and the deterministic JavaScriptCore follow-up collection needed by the CTS finalizer contract. The refreshed base is merged as a parent so the existing Worker review history remains intact; the resulting tree exactly matches the locally tested restack.

The Worker branch composes the independently reviewable browser-polyfill work while those PRs are pending:

Worker initializes those targets in browser-compatible dependency order. It no longer embeds reduced private copies of Streams, Blob.stream, Response, decompression, or IndexedDB, and no longer links zlib itself.

Browser-compatible Worker surface

  • One native thread, AppRuntime, engine realm, and event loop per dedicated Worker.
  • Worker, EventTarget, Event, MessageEvent, ErrorEvent, DOMException, handler attributes, postMessage, and terminate.
  • DedicatedWorkerGlobalScope with self, location, name, navigator, close, synchronous importScripts, timers, fetch/XHR, URL, Blob/File, text codecs, WebSocket, performance, abort APIs, Streams, CompressionStream, and IndexedDB.
  • Structured clone for cyclic/shared graphs, special numbers, BigInt, Date, RegExp, Map, Set, Error, ArrayBuffer/DataView, and typed arrays.
  • Transfer-list validation and N-API v7 ArrayBuffer detachment, including duplicate, invalid, and already-detached DataCloneError paths.
  • Root-confined app:/// and relative scripts/assets, explicit file://, percent-encoded data:, and a thread-safe packaged-asset resolver.
  • Worker-relative fetch inputs resolved against the Worker location.

System JavaScriptCore uses its execution-time-limit ABI to interrupt top-level infinite evaluation. No JSC binary is downloaded or vendored: the Linux lanes install the OS libjavascriptcoregtk-4.1-dev package. QuickJS remains the additional race/conformance test engine.

Visualization startup and playback integration

The reduced integration fixture is pinned to rebeckerspecialties/webapp@3765b13, specifically GithubPortfolio.worker.ts, its parent interop, and IndexedDBCache.

The source trace found two blockers beyond the base Worker interface:

  1. pipelineCreate synchronously constructs the IndexedDB cache, so a missing indexedDB stops startup before playback can fall back to live data.
  2. Prerecorded playback fetches a Worker-relative ${owner}.${repo}.gz, then runs Blob.stream().pipeThrough(new DecompressionStream("gzip")), constructs a Response, parses the cache, and writes the records in one IndexedDB transaction.

The fixture constructs a named { type: "module" } Worker, queues pipelineCreate and subscribe while the bundle is evaluating, hydrates rebeckerLoaderCacheStore, and plays three 128-item streams. It verifies relative fetch, gzip rejection behavior, transaction completion, Date/Map/Set/cyclic/shared-alias clone fidelity, 384 updated items, abort reasons, and browser-style constructor feature detection.

The deployed GitHub Pages origin itself was not launched in this environment, so this is a source-pinned, app-derived integration test rather than a claim about an end-to-end XR render. The tested contract reaches pipelineCreated, emits the three playback streams, and preserves the data required by the XR consumer.

Browser regression hardening

The regression fixtures carry their source links and guard known browser implementation failures:

The JSC sanitizer pass also exposed a per-stream implementation cost in Blob.stream(): capturing node-addon-api callbacks attach hidden callback-data properties to each generated function, causing two extra wrappers and JSC Function structure transitions per stream. The standalone Blob branch now uses a shared internal stream-source prototype, so its native pull/cancel callbacks are created once per realm instead of once per stream; the same commit is composed here and keeps the existing BYOB, cancellation, and lifetime tests.

The restack also caught an integration-specific variant of the same “polyfill replaces a host primitive” antipattern: Worker event glue replaced IndexedDB’s existing DOMException, breaking DataCloneError instanceof DOMException. Worker now preserves a host constructor, with a dedicated native regression test.

Tests

The vendored Worker subset pins web-platform-tests/wpt@4809b72, its license, and testharness.js.

Fresh local QuickJS results after the restack:

  • Worker host/composition + WPT/app-derived integration: 2/2 pass.
  • Streams/Compression/IndexedDB/Fetch initializer and host-preservation tests: 6/6 pass.
  • QuickJS Node-API throw-status contract: 1/1 pass.
  • Combined native Worker/browser-runtime regression filter: 9/9 pass.
  • Focused Headers/Response/Streams/Compression/Blob/IndexedDB JavaScript regressions: 81/81 pass.
  • The target builds cleanly with the full Worker dependency graph.
  • GitHub Worker matrix: 3/3 pass (system JSC, JSC ASan/UBSan, and QuickJS TSan).

The general JavaScript suite was also started, but this container’s external XHR cases block on network access before reaching the remaining suites; the focused pass above runs the complete changed-feature set. GitHub Actions supplies the system-JSC, ASan/UBSan, and QuickJS-TSan gates.

Deliberate first-cut boundaries

  • type: "module" accepts self-contained script-compatible bundles; imports/exports must be flattened because public JSC has no module-loader hook.
  • IndexedDB is complete enough for object stores, indexes, cursors, key ranges, upgrades, rollback, and storage clone, but remains per-realm and in-memory. Durable or cross-realm storage is a separate backend concern.
  • The existing native fetch transport is still not the complete Fetch standard.
  • unhandledrejection synthesis, SharedArrayBuffer, MessagePort transfer, Blob object URLs, nested Workers, and full module-graph loading are not included yet.
  • The legacy JSI adapter has no v7 detachable-ArrayBuffer ABI, so Worker remains unavailable there.
  • Tight-loop interruption is implemented for system JSC first; other engines stop cooperatively between dispatches until their native interrupt hooks are connected.

…ction when the receiver was undefined, so the VM forcibly substituted the global object even in strict mode. The new implementation always routes through Function.prototype.call, preserving the exact thisArg. This only affected JSC: Chakra already pushes recv onto the argv array before invoking JsCallFunction, and V8 hands the raw recv value to Function::Call. Neither engine coerces in strict mode, so no additional fixes were required.
…bug fix in strict mode was actually found by the suite! The failing behavior was exercised by Tests/NodeApi/test/js-native-api/3_callbacks/test.js. New cmake targets emit a node-lite binary and a NodeApiTests binary, all currently enabled tests for currently supported NAPI v5 pass on Mac. Next step is to enable them for running in Android simulator.
…x handles during instrumentation, causing crashes, and now route console output through the new NodeLiteRuntime::Callbacks. On Android we forward stdout/stderr to logcat via callbacks to work around this for now. Added Android-specific shims (node_lite_android.cpp, child_process_android.cpp) so native module loading uses dlopen and JS child_process.spawnSync safely reports “unsupported”. Extended the Node‑API harness to allow in-process execution: RunNodeLiteScript captures output, SetNodeApiTestEnvironment lets the JNI layer provide a base directory and asset manager, and the GTest registration path uses that configuration instead of shelling out to the node_lite executable
…-- a use-after-free. Will check sanitizers under Android next
Two build-restoration fixes (no behavior/impl or NAPI-version changes), needed
after rebasing onto upstream HEAD and building with the current Xcode/libc++:

- node_lite: NodeApi::CallFunction took std::span<napi_value> but is only ever
  called with braced-init-lists ({a,b,c}). Newer libc++ correctly rejects
  constructing a non-const std::span from an initializer_list (that ctor is
  C++26). Switch the parameter to std::initializer_list<napi_value> (begin()
  yields the const napi_value* napi_call_function wants).

- Tests/NodeApi: the POST_BUILD copy_directory of each .node runs as an Xcode
  script phase BEFORE Xcode's implicit CodeSign phase signs the original, so the
  copied addons that node_lite/NodeApiTests dlopen are unsigned on a clean build
  and macOS refuses to load them. Ad-hoc sign the copies directly (APPLE only).
…mulator)

Build-restoration fixes for the Android in-process NodeApi harness after rebasing
onto upstream HEAD (no impl/NAPI-version changes). Each was a latent break in the
napi-tests Android integration, surfaced by a clean build on a current toolchain:

- CMakeLists.txt: drop the AndroidExtensions Globals.cpp 'patch' step. It file(COPY)'d
  patches/AndroidExtensions/Globals.cpp, which was never committed in any ref (author's
  local-only file). Upstream uses a newer AndroidExtensions pin and needs no patch.
- build.gradle: bump default ndkVersion 23.1.7779620 -> 28.2.13676358 (matches CI's
  NDK_VERSION). NDK 23's libc++ can't compile googletest 1.17.0's <=> usage. Also map the
  Android sanitizer flag JSR_ENABLE_ASAN -> ENABLE_SANITIZERS (the upstream option kept
  during the rebase).
- Tests/NodeApi/CMakeLists.txt: use ${JsRuntimeHost_SOURCE_DIR} instead of
  ${CMAKE_SOURCE_DIR} for Core/Node-API include paths. On Android JsRuntimeHost is added
  as a subdirectory of the app, so CMAKE_SOURCE_DIR was the app dir (headers not found);
  the project-scoped var is correct in both standalone (macOS) and nested (Android) builds.
- Tests/NodeApi/CMakeLists.txt: allow the .node modules to link with unresolved napi_*
  symbols on Android (-Wl,--unresolved-symbols=ignore-all), the ELF equivalent of Apple's
  -undefined dynamic_lookup; they bind at dlopen time from the host (UnitTestsJNI).
- Shared.cpp: gate the Android NodeApi-harness block on NODE_API_AVAILABLE_NATIVE_TESTS
  (defined only by UnitTestsJNI) so the standalone UnitTests target -- built but unused on
  Android -- doesn't try to compile AndroidExtensions/NodeApi code it doesn't link.
The instrumented run aborted with 'use of deleted global reference': the harness fell
back to android::global::GetAppContext() (GetFilesDir -> GetObjectClass) whose JNI global
ref is not valid during the instrumented run. JNI.cpp now computes a writable base dir from
the still-valid instrumentation Context and passes it plus the native AAssetManager to
SetNodeApiTestEnvironment() before RunTests() -- the wiring the harness was designed for
(see e1fce6b) but which was never actually connected.

This removes the crash and lets ConfigureNodeApiTests run. NOTE: on-device execution of the
NodeApi conformance tests is still not achieved -- CopyAssetsRecursive relies on
AAssetManager subdirectory enumeration (AAssetDir_getNextFileName lists files only, not
dirs) so the nested test tree isn't copied, and the native .node modules are neither
packaged nor loadable from an app-writable dir on API 29+. Tracked as follow-up.
Before this, the instrumented run passed vacuously -- no NodeApi tests ran. Several
layered fixes get them executing on the emulator (macOS path unchanged: still 12/12):

#1 Asset enumeration: AAssetManager can't list subdirectories, so CopyAssetsRecursive
   copied nothing. copyNodeApiTests now emits a file manifest (manifest.txt -- not a
   dotfile, which aapt would drop) and Shared.cpp copies each listed file.

#2 Native module packaging/loading: build each addon as lib<name>.so on Android so AGP
   packages it into lib/<abi>/ (nativeLibraryDir, the only dlopen-able location on API
   29+); node_lite_android loads it by soname; ResolveModulePath resolves the (on-disk
   absent) .node so LoadNativeModule runs.

V8 lifecycle (in-process node_lite shares the host's V8): reuse the host's already-
   initialized V8 platform (fixes 'Wrong initialization order'); hold a Locker +
   Isolate::Scope so multi-isolate access is locked (fixes 'Entering the V8 API without
   proper locking').

KNOWN REMAINING (tracked): node_lite calls Node-API outside any napi callback during
   NodeLiteRuntime::Initialize/script execution, which on V8 needs a live HandleScope +
   current Context. v8::HandleScope/Context::Scope are stack-only (operator new is
   private) so they can't be held across the holder; this needs a scope-wrapping rework
   of node_lite's V8 entry points (or napi_open_handle_scope + context enter). Until then
   the on-device native tests segfault in napi_create_object.
…eate_object segfault)

NodeApiEnvScope -> jsr_open_napi_env_scope was a no-op stub: it allocated a scope
struct but never entered the env's V8 isolate/context. On JSC that's fine (the env
carries its context explicitly), but on V8 node_lite then calls Node-API outside any
napi callback with no *current context*, so napi_create_object -> v8::Object::New(isolate)
segfaulted during NodeLiteRuntime::Initialize. Enter the env's context on open and exit
it on close (Android only). The in-process V8 runtime now initializes and runs tests.
… error

Step toward in-process error handling: ExitOnException was noexcept, but the in-process
runner installs a fatal handler that throws NodeLiteFatalError (rather than std::exit) so
the harness can turn a JS error into a ProcessResult. Throwing from the noexcept function
std::terminate'd the test process. Dropped noexcept so it propagates to RunNodeLiteScript.

(Partial: other noexcept teardown paths -- NodeApiHandleScope/NodeApiEnvScope dtors calling
NODE_LITE_CALL, and the env-holder dtor's onUnhandledError -> ExitWithJSError -- can still
throw during unwinding when a test errors. Full in-process error-path exception-safety is
the remaining Android item.)
NodeApiHandleScope/NodeApiEnvScope destructors used the throwing NODE_LITE_CALL, and the
JsRuntimeHostEnvHolder destructor's onUnhandledError can invoke the throwing in-process fatal
handler -- both std::terminate if they fire while a NodeLiteFatalError is unwinding. Make the
scope dtors ignore the close status and wrap onUnhandledError in try/catch.

Correct robustness fixes, but they do NOT yet resolve the remaining in-process failure: when a
test errors, a *second* NodeLiteFatalError is thrown during unwinding (double-exception ->
std::terminate). The escaping throw site isn't visible in the tombstone (stack already unwound)
and needs on-device lldb to pinpoint. macOS unaffected (12/12).
…winding

Don't re-throw NodeLiteFatalError from the in-process fatal handler when std::uncaught_exceptions()
> 0, to avoid a double-exception std::terminate. (Correct hardening, but the remaining in-process
abort is a *single* uncaught NodeLiteFatalError escaping RunNodeLiteScript's catch -- a scope-exit
destructor throw on a test that leaves a pending exception; needs on-device lldb to pinpoint.)
…(fixes terminate)

THE fix for the in-process abort. ExitWithJSError / ExitWithJSAssertError / ExitWithMessage
were declared noexcept. With the default fatal handler they call std::exit (never throw), but
the in-process runner installs a handler that *throws* NodeLiteFatalError (caught by
RunNodeLiteScript and turned into a ProcessResult). A throw crossing a noexcept boundary is an
immediate std::terminate -- so when any test errored (e.g. the expected-error basics tests
throw_string/mustcall_failure), the whole instrumented run aborted instead of reporting a
result. Removing noexcept lets the throw unwind to the catch. Confirmed on the emulator via a
temporary _Unwind_Backtrace probe (now removed): the throw stack was
HandleFatalError <- ExitWithMessage(noexcept!) <- ExitWithJSError <- RunTestScript <- RunNodeLiteScript.

Net effect: the in-process Android run no longer aborts; the js-native-api v5 tests (2-5) pass;
the remaining failures are the basics harness self-tests, run through the generic fixture rather
than the specialized test_basics.cpp path macOS uses. macOS unaffected (still 12/12).
… napi

The js-native-api conformance addons are dlopen'd in-process by the Android
harness and import napi_* from the host (libUnitTestsJNI.so). The host is
loaded RTLD_LOCAL by System.loadLibrary, and bionic's linker-namespace model
does not surface its statically-linked (but exported) napi_* symbols to a
dlopen'd module -- so the addon cannot bind them at load time. Post-hoc
RTLD_GLOBAL promotion of the host is a no-op on bionic (confirmed on device:
the module dlopen still returns NULL with the host re-opened RTLD_GLOBAL).

Making these tests runnable on Android requires building napi as a shared
library (libnapi.so) depended on by both the host and the addons -- a
packaging change affecting every Android consumer, deferred to a separate
change per the v5-suite-in-place scope. Until then, skip the in-process addon
tests on Android with a clear reason; macOS runs the full v5 addon suite
(12/12) as the reference.

This unblocks the Android suite: it now builds, the in-process harness runs
without aborting, and the suite passes (addon tests reported SKIPPED).
… in-process)

Dynamic .node loading is never shipped to the Play / Quest stores, and bionic won't resolve a
dlopen'd addon's napi_* imports against the System.loadLibrary-loaded host anyway (the addon carries
no DT_NEEDED for napi; RTLD_GLOBAL host promotion is a no-op on bionic). Rather than make napi a
shared library for every Android consumer (tracked separately, task #9), compile the conformance
addons directly into the host (UnitTestsJNI) and resolve them in-process.

To link several addons into one binary without symbol clashes:
- node_api.h: make NODE_API_MODULE_REGISTER_FUNCTION / _GET_API_VERSION_FUNCTION overridable.
- entry_point.h (JSR_NODE_API_STATIC_LINK): give Init internal linkage and emit a per-addon load-time
  constructor that self-registers its uniquely-suffixed registrar/version functions with the host.
- The Android CMakeLists compiles each addon as an OBJECT library with per-module unique entry-point
  names and links them into UnitTestsJNI.
- node_lite_android LoadFunction resolves entry points from the in-process static registry by module
  name instead of dlopen+dlsym.

Removes the Android GTEST_SKIP. The 4 v5 js-native-api conformance tests now execute in-process and
PASS on Android (2_function_arguments, 3_callbacks, 4_object_factory, 5_function_factory). macOS is
unchanged (the desktop dynamic .node path uses the #else branches).
The conformance suite runs gtest in-process; its results (RUN/OK/FAILED and failure
file:line:message) went to stdout, which Android discards -- leaving only the JUnit "expected 0,
was 1" with no detail. Pump stdout/stderr to logcat (tag NodeApiTests) so test output and any
pre-crash native context are visible via `adb logcat -s NodeApiTests`.
…by static linking)

The conformance addons are statically linked into the in-process Android test host (f32130e), so the
standalone SHARED .so + -Wl,--unresolved-symbols=ignore-all + lib<name>.so naming that the old
dlopen-on-Android path needed are dead. add_node_api_module now early-returns on Android and is a
clean desktop-only MODULE .node helper. Also fixes a stale node_lite comment describing the
abandoned soname-dlopen path.

No functional change on desktop (MODULE .node, -undefined dynamic_lookup, POST_BUILD staging,
codesign all preserved); macOS still 12/12.
V8Platform::EnsureInitialized() became a no-op once we found the host AppRuntime already initializes
V8's process-global platform; the class and its unused init_flag_/platform_ members were left over
from the abandoned platform-init attempt. Fold the (still-important) "don't re-init the platform"
rationale into a comment at the isolate-creation site, and drop the dead class plus the now-unused
<mutex> / <libplatform> includes. No behavior change; Android still 4/4 js-native-api.
…d libnapi.so

Replaces the interim static-link-into-host approach with the dynamic .node model used by
nodejs/node-api-cts (add_node_api_cts_addon), so the Android suite and a future node-api-cts
migration share one addon model.

- Core/Node-API: build napi as a SHARED library (libnapi.so) on Android. It exports all 106 napi_*
  (default visibility -- no global -fvisibility=hidden), and the host plus every addon depend on the
  one libnapi.so via a real DT_NEEDED, so there is a single napi instance. Static elsewhere.
- The conformance addons are again standalone SHARED lib<name>.so (packaged into nativeLibraryDir),
  now linking napi (DT_NEEDED libnapi.so) instead of -Wl,--unresolved-symbols=ignore-all.
- node_lite_android resolves entry points via dlopen(soname)+dlsym again; the addon's napi_* bind
  from libnapi.so at load. Reverts the static-link infra (entry_point.h JSR_NODE_API_STATIC_LINK
  branch, node_api.h overridable registrar macros, the host's per-addon OBJECT libraries).

Verified on device: lib2_function_arguments.so has DT_NEEDED [libnapi.so], its napi_* are imports,
libnapi.so exports the 106 napi_*, and all 4 v5 js-native-api tests pass in-process. macOS unchanged
(12/12; desktop keeps static napi + dlopen'd MODULE .node).
Replace the hand-rolled pipe+thread stdout pump (added while bringing up the in-process Node-API
harness) with android::StdoutLogger::Start()/Stop() from AndroidExtensions, which the rest of the
UnitTests host already uses. Same effect -- the in-process gtest output (incl. failure
file:line:message) is visible in logcat (tag StdoutLogger) -- with less bespoke code.

Verified on emulator: 8/8 UnitTests pass incl. 4/4 js_native_api, gtest output present in logcat.
Observe standard ReadableStream consumption paths with weak per-stream state instead of depending on web-streams-polyfill's private _disturbed field. Instrument stream readers and piping once during Fetch initialization, preserve stream identity, and cache the initialized Fetch implementation so repeated initialization does not stack wrappers or replace its internal state.

Treat Headers and Response as an implementation pair when either host global is missing or null. Harden native initialization tests against unhandled N-API failures and add WPT-derived coverage for reads, cancellation, piping, and streams disturbed before Response construction.
matthargett pushed a commit that referenced this pull request Sep 1, 2026
### Problem

`napi_throw`, `napi_throw_error`, `napi_throw_type_error` and
`napi_throw_range_error` returned `napi_pending_exception` after
successfully scheduling the throw.

In Node-API that status means *"this call failed because an exception
was already pending"*, not *"a throw is now pending"*. The upstream
implementation returns `napi_clear_last_error(env)` (i.e. `napi_ok`).

Because the QuickJS port reported failure,
`Error::ThrowAsJavaScriptException` in `napi-inl.h` took its failure
branch on **every** native throw:

```cpp
napi_status status = napi_throw(_env, Value());
#ifdef NAPI_CPP_EXCEPTIONS
    if (status != napi_ok) {
      throw Error::New(_env);   // consumes the exception that was just set
    }
#endif
```

`Error::New(env)` calls `napi_get_and_clear_last_exception`, so the
pending JS exception is discarded and a fresh C++ exception is thrown
out of `details::WrapCallback`. `ExternalCallback::Callback` then
catches it, observes `!JS_HasException(ctx)`, and rebuilds the error
from `e.what()`.

By that point the `HandleScope` opened by `ThrowAsJavaScriptException`
has been destroyed during unwinding, so stringifying the message reads
freed memory.

### Impact

Two symptoms, both of which reproduce today:

1. **Wrong error surfaced to JS.** The real error is replaced by
`InternalError: Uncaught C++ exception: <message>`. Every native throw
on QuickJS is affected, so `err.name` and `err instanceof TypeError` are
wrong throughout.
2. **Use-after-free.** On Linux this segfaults. Backtrace from a
BabylonNative CI core dump:

```
#0  js_dup                       quickjs.c:1628          <-- SIGSEGV
#1  js_force_tostring            quickjs.c:4813
#3  JS_ToCStringLen
#4  napi_get_value_string_utf8   js_native_api_quickjs.cc:696
#5  Napi::String::Utf8Value      napi-inl.h:1118
#7  Napi::Error::Message         napi-inl.h:3087
#8  Napi::Error::what            napi-inl.h:3157
#9  ExternalCallback::Callback   js_native_api_quickjs.cc:164
```

The `JSValue` being stringified carries `JS_TAG_STRING` with an
unaligned, freed pointer.

I instrumented the `catch` in `ExternalCallback::Callback` in a
BabylonNative QuickJS build and confirmed that **all ~50 native throws**
in that test run escaped `WrapCallback` with `hasExc=0`. After this
change the count is 0.

### Fix

Return `napi_ok` from the four throw entry points, matching upstream.
The exception stays pending, `WrapCallback` returns normally, and the
fragile `e.what()` fallback is never entered.

### Test

Added a strict assertion to the existing `URLSearchParams.set()` arity
throw, checking the error type and exact message rather than a
substring. The pre-existing `.to.throw()` test could not catch this,
because `"Uncaught C++ exception: <msg>"` still *contains* the expected
substring.

Verified on Linux QuickJS (RelWithDebInfo):

| | result |
|---|---|
| without the C++ change | `expected 'InternalError' to equal 'Error'` —
212 passing, **1 failing** |
| with the C++ change | **213 passing**, 10/10 gtest |

Also verified in a BabylonNative QuickJS build on Windows: 21/21 gtest,
49 JS assertions, exit 0, and zero escapes from `WrapCallback`.

Co-authored-by: Branimir Karadzic <branimirkaradzic@gmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 88569c10-a7ff-4373-9a58-afa9c68b8c09
@matthargett
matthargett force-pushed the napi-v7 branch 7 times, most recently from e813292 to 434fd79 Compare September 1, 2026 03:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant